/**!
Node的本地搜索引擎，通过NodeIndex, 建立rabbit的Doc Schema，
并持有Index, IndexWriter，IndexReader。
以完成索引文件和检索文件的功能。
*/
use tokio;
use std::io::Read;
use std::path::Path;
use std::fs::File;
// use serde::{Deserialize, Serialize};
use tantivy::{doc, Snippet, SnippetGenerator};
use tantivy::collector::TopDocs;
use tantivy::query::{TermQuery, Query, RegexQuery, FuzzyTermQuery};
use tantivy::schema::*;
use tantivy::query::QueryParser;
use anyhow::{anyhow, bail};

#[cfg(feature = "tracelog")]
use tracing::{debug, error, info};
#[cfg(feature = "rotatelog")]
use log::{debug, error, info, warn};

mod nodeindex;
pub use nodeindex::*;
use gfsc::*;

/**
 * 对指定的（文本）文件进行索引创建。
 * docid_code is the sha256 hash code by file content. 
 * docid_code唯一，避免相同内容的不必要重复索引。
 * 
 */
pub async fn build_index(index: &NodeIndex, docid_code:&str, filename:&str, pathfilename: &str) -> Result<()> {
        
    // Oops our frankenstein doc seems misspelled
    if index.doc_is_exist( &docid_code)? {
        error!(?filename, " Have been exist!");
        bail!("have been exist!")
    }

    let mut content = String::new();
    let path = Path::new(pathfilename);
    // 以只读方式打开路径，返回 `io::Result<File>`
    File::open(&path)?
        .read_to_string(&mut content)?;

    let (docid, fname, body) = index.get_schema_tables();
    // let docid_term = Term::from_field_text(docid_field, docid);    
       
    let mut index_writer = index.get_writer().write().await;
    index_writer.add_document(doc!(
        docid => docid_code,  
        fname => filename,
        body => content
        ))?;                
    index_writer.commit()?;  
    Ok(())   

}

/**
 * 针对当前Node索引的特定内容搜索（类型qtype可指定，默认QueryParser）
 * 其他可选类型：RegexQuery，TermQuery，FuzzyTermQuery 
 * 特别注意：通过类型指定，可以分别就fname，或content进行对应的索引。
 *    
 */
pub async fn node_search(node_index: &NodeIndex, qry:&str, qtype:&str, page:usize) -> Result< Vec<SearchDoc>> {
    debug!(?qry, ?qtype, ?page);
    let index = node_index.get_index();       
    let reader = node_index.get_reader();
    let searcher = reader.searcher();

    let (docid, fname, body ) = node_index.get_schema_tables();
    let query:Box<dyn Query> = match qtype {
        "regexf" => {
            Box::new(RegexQuery::from_pattern(qry, fname)?)
        },
        "regex" => {
            Box::new(RegexQuery::from_pattern(qry, body)?)            
        },
        "termf" => { 
            error!("try to use Term Query on filename");
            let query = Term::from_field_text(fname, qry);            
            Box::new(TermQuery::new(query.clone(), IndexRecordOption::Basic))    
        },
        "term" => {             
            let query = Term::from_field_text(body, qry);            
            Box::new(TermQuery::new(query.clone(), IndexRecordOption::Basic))    
        },
        "fuzzyf"=> {
            let term = Term::from_field_text(fname, qry);
            Box::new(FuzzyTermQuery::new(term, 1, true))
        },
        "fuzzy"=> {
            let term = Term::from_field_text(body, qry);
            Box::new(FuzzyTermQuery::new(term, 1, true))
        },
        _  => {
            debug!(?qtype, " default use QueryParser");
            let query_parser = QueryParser::for_index(&index, vec![docid, fname, body]);
            Box::new(query_parser.parse_query(qry)?)
        },
    };

    let top_docs = searcher.search(&query, &TopDocs::with_limit(30))?;
    let mut snippet_generator = SnippetGenerator::create(&searcher, &*query, body)?;
    snippet_generator.set_max_num_chars(72); // 3*2*x
    let mut rets : Vec<SearchDoc> = vec![];
    for (score, doc_address) in top_docs {
        let doc = searcher.doc(doc_address)?;
        let snippet = snippet_generator.snippet_from_doc(&doc);        
        
        let mut rdoc = SearchDoc::default();
        rdoc.docid = doc.get_first(docid).ok_or( anyhow!("get field error"))?
            .as_text().ok_or(anyhow!("get field error"))?
            .to_string();
        rdoc.fname = doc.get_first(fname).ok_or(anyhow!("get field error"))?
            .as_text().ok_or(anyhow!("get field error"))?
            .to_string();

        if  snippet.fragment().as_bytes().len() > 0 {
            rdoc.content =  highlight(&snippet)?;         
            /* 
            if !api {
            
            }else{
                rdoc.content = highlight_console(&snippet)?;  
            }             */
        }else{
            rdoc.content = highlight_snippert_bugfix(doc.get_first(body).ok_or(anyhow!("get field error"))?
                .as_text().ok_or(anyhow!("get field error"))?, qry)?;              
        }      

        rdoc.score = score;
        rets.push(rdoc);
    } 
    Ok(rets)
}

/**
 * 针对命中搜索的结果片段，进行命中词的修饰（<mark></mark>或 Colour::Red::paint），以及多命中段的串连。
 * 
 */
fn highlight(snippet: &Snippet) -> Result<String> {

    let mut result = String::new();
    let mut start_from = 0;
    let max_len = snippet.fragment().as_bytes().len();
    debug!("fragment length = {} ", max_len);
    let mut num = 0;
    for fragment_range in snippet.highlighted() {        
        if start_from <= fragment_range.start && fragment_range.start  < max_len {
            result.push_str( &snippet.fragment()[start_from..fragment_range.start]);         
            num = 0;
        }        
        if num > 0 {
            result.push_str(" | ");                            
        }

        result.push_str(r##"<mark>"##);        
                         
        result.push_str(&snippet.fragment()[fragment_range.clone()]);       
        result.push_str("</mark>");
        num += 1;
        start_from = fragment_range.end;        
    }    

    if start_from < max_len {
        result.push_str( &snippet.fragment()[start_from..]);          
    }
    debug!("{}", result);
    Ok(result)
}

/**
 * 早期tantivy 0.18存在的 Snippert bugfix.
 *  
 */
fn highlight_snippert_bugfix(body: &str, qry: &str) -> Result<String> {
    let mut result = String::new();
    const TERM_LEN : usize = 40;
    result.push_str("[ Snippert bugfix ] ");
    if let Some(fdx) = body.find(qry) {        
        let left = String::from(&body[0..fdx]);        
        let right = String::from(&body[fdx+qry.len()..]);        
        let mut left_pos:usize = 0;
        let ll = left.chars().count();
        if ll  > TERM_LEN {            
            for i in 0..TERM_LEN {
                left_pos += left.chars().nth(ll-1-i).ok_or(anyhow!("get field error"))?
                   .to_string().len();               
            }            
            left_pos = left.len() - left_pos;
        }

        let mut right_pos:usize = 0;
        if right.chars().count()> TERM_LEN {            
            for i in 0..TERM_LEN {
                right_pos +=  String::from(right.chars().nth(i).ok_or(anyhow!("get field error"))?).len();
            }            
        }else{
            right_pos = right.len();
        }
        
        debug!(?left_pos, ?fdx, ?right_pos);        
        result.push_str(&body[left_pos..fdx]);                   
      
        let term = format!("<mark>{}</mark>",qry);    
        result.push_str(&term);            
      
        result.push_str( &right[..right_pos] )
    }
    Ok(result)
}

